-- | Module:    Hexena.Modules.BlockEntropy
--
-- Blockentropy functions, this will calculate the blockentropy and do
-- all the reporting.
module Hexena.Modules.BlockEntropy ( BlockEntropy (..)
                                   , alreadyAnalysed
                                   , analyse
                                   , checkPrerequisites
                                   , report
                                   , reportFromIFF
                                   , serialise
                                   ) where

-- TODO remove, see loadBLEN
import Data.Binary.Get
import Data.Binary.Put

import Control.Monad (when)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.List
import Data.Maybe
import Data.IFF
import Data.Word (Word8, Word32)
import qualified Graphics.Gnuplot.Simple as GP

import Paths_hexena (getDataFileName)
-- import Hexena.Chunks
import Hexena.Math.Statistics
import Hexena.Math.Statistics.Internal
import Hexena.Reporting
import System.IO (hClose, openTempFile)

data BlockEntropy = BlockEntropy {
      entropies :: [Double],
      avgEntropy :: Double,
      sdevEntropy :: Double
    } deriving (Show)

bodyID :: ChunkId
bodyID = chunkIdFromString "BODY"

blenID :: ChunkId
blenID = chunkIdFromString "BLEN"

-- | Check if the analysis results have been stored in the hexena file.
alreadyAnalysed :: [T] -> Bool
alreadyAnalysed = isJust . find ((blenID ==) . chunkId)

-- | Check if all the prerequisites for an analysis are met.
checkPrerequisites :: [T] -> Bool
checkPrerequisites = isJust . find ((bodyID ==) . chunkId)

-- | Analyse and return all the results. If the file has been analysed
-- | previously and the data is stored in the hexena file then return
-- | these resutls.
analyse :: [T] -> IO BlockEntropy
analyse cs = case find ((blenID ==) . chunkId) cs of
               Nothing -> analyse' cs
               Just x -> let entropies = fromJust $ loadBLEN x
                             m = mean entropies
                             s = stddev entropies
                         in return $ BlockEntropy entropies m s

-- | Serialise the result data for saving in the IFF file.
serialise :: BlockEntropy -> T
serialise = saveBLEN

report :: BlockEntropy -> IO Report
report be = do t <- getDataFileName "blockentropy.template.tex" >>= readFile
               (tmp, h) <- openTempFile "/tmp" "blockentropy.XXXXXX"
               hClose h -- have to close handle as I get resource busy...
               GP.plotList [GP.EPS tmp] $ entropies be
               blockgraph <- readFile tmp
               return $ Report [("blockentropy", "\\input{blockentropy.tex}")]
                          (replace [ ("average", show $ avgEntropy be)
                                   , ("stdev", show $ sdevEntropy be) ] t)
                          "blockentropy"
                          [(blockgraph, "blockentropy.eps")]

reportFromIFF :: [T] -> IO (Maybe Report)
reportFromIFF x = if alreadyAnalysed x then analyse x >>= report >>= return . Just else return Nothing

blockify :: [Word8] -> [[Word8]]
blockify = unfoldr f
    where sp = splitAt 256
          f x = if null x then Nothing else Just $ sp x

analyse' :: [T] -> IO BlockEntropy
analyse' cs = let body = fromJust $ find ((bodyID ==) . chunkId) cs
                  blocks = blockify $ B.unpack $ content $ chunk body
              in do histograms <- mapM calcHist blocks
                    let entropies = map calculateE histograms
                    return $ BlockEntropy entropies (mean entropies) (stddev entropies)


-- TODO: move this to Chunks.hs and break cyclic dependencies
-- TODO: even more important: think hard! What is the best way? To
-- have all the stuff per module or to put the chunk/iff stuff in a
-- separate file, the reporting stuff in a separate file, etc.?
loadBLEN :: T -> Maybe [Double]
loadBLEN x = if chunkId x == blenID then
                 Just $ map f $ interpret $ content $ chunk x
              else
                 Nothing
    where interpret x = runGet gF $ BL.fromChunks [x]
          gF :: Get [Word32]
          gF = do empty <- isEmpty
                  if empty then return [] else do w <- getWord32be
                                                  x <- gF
                                                  return (w:x)
          f x = fromIntegral x / (2^32 - 1) * 8.0

saveBLEN :: BlockEntropy -> T
saveBLEN x = Cons (chunkIdFromString "BLEN") $ Chunk $ B.concat $ BL.toChunks $ runPut pF
    where pF = mapM_ (putWord32be . f) $ entropies x
          f e = floor $ e / 8.0 * (2^32 - 1)

